package dslab.util;

import dslab.enums.Outcome;
import dslab.nameserver.INameserverRemote;

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.rmi.RemoteException;
import java.util.Enumeration;

public class DynamicDMTPClient implements Runnable {

    private final String componentId;
    private final Message message;
    private final boolean logging;

    private final String domain;
    private final Pair<InetAddress, Integer> monitoring;
    private final INameserverRemote root;
    private final boolean notifySenderOnError;

    private String host;
    private int port;

    /**
     * creates a new DynamicDMTPClient with domain lookup capabilities (re-uses DMTPClient)
     *
     * @param componentId string representation of component
     * @param domain host domain
     * @param message message to transmit
     * @param monitoring monitoring information as <code>Pair<InetAddress, Integer></code>
     * @param root remote object for the root nameserver
     * @param notifySenderOnError whether to loop-back transmission errors to the sender
     * @param logging whether to print debug statements or not
     */
    public DynamicDMTPClient(String componentId, String domain, Message message, Pair<InetAddress, Integer> monitoring,
                      INameserverRemote root, boolean notifySenderOnError, boolean logging) {
        this.componentId = componentId;
        this.domain = domain;
        this.message = message;
        this.monitoring = monitoring;
        this.root = root;
        this.notifySenderOnError = notifySenderOnError;
        this.logging = logging;
    }

    @Override
    public void run() {
        // make lookup, stop if there was a failure
        if (!dynamicDomainLookup()) {
            sendNotification();
            return;
        }

        // send monitoring statistics
        sendMonitoringStatistics(host, port);

        // run the actual client
        DMTPClient client = new DMTPClient(
            componentId,
            message,
            host,
            port,
            logging
        );
        Outcome outcome = client.call();

        // in case of an error, send error notification
        if (outcome != Outcome.Success)
            sendNotification();
    }

    private boolean dynamicDomainLookup() {
        String hostWithPort;
        try {
            System.out.printf("trying to look up domain %s%n", domain);
            hostWithPort = root.lookupDomain(domain);
            if (hostWithPort == null) {
                sendNotification();
                if (logging)
                    System.err.printf(
                        "@%s: a fatal error has occurred: the domain %s was not able to be located",
                        componentId,
                        domain
                    );
                return false;
            }

            // query relevant information and return
            host = hostWithPort.split(":")[0];
            port = Integer.parseInt(hostWithPort.split(":")[1]);
            return true;
        } catch (RemoteException exception) {
            if (logging)
                System.err.printf(
                    "@%s: a fatal error has occurred: the domain %s has produced an RMI-Error",
                    componentId,
                    domain
                );
            exception.printStackTrace();
            sendNotification();
            return false;
        } catch (NumberFormatException exception) {
            if (logging)
                System.err.printf(
                    "@%s: a fatal error has occurred: the domain %s has an illegal port number%n",
                    componentId,
                    domain
                );
            exception.printStackTrace();
            sendNotification();
            return false;
        }
    }

    private void sendMonitoringStatistics(String host, int port) {
        // opt out of monitoring if no monitoring server was supplied
        if (monitoring == null)
            return;

        // send a UDP packet into the void in hopes of reaching a monitoring server
        try (DatagramSocket client = new DatagramSocket()) {
            // bookkeeping
            String payload = String.format("%s:%d %s%n", host, port, message.sender);
            byte[] buffer = payload.getBytes(StandardCharsets.UTF_8);
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, monitoring.key(), monitoring.value());
            client.send(packet);
            if (logging)
                System.out.printf(
                    "@%s: monitoring \"%s:%d %s\" at %s:%d%n",
                    componentId,
                    host,
                    port,
                    message.sender,
                    monitoring.key(),
                    monitoring.value()
                );
        } catch (SocketException exception) {
            if (logging)
                System.err.printf(
                    "@%s: unable to open UDP socket for sending: %s:%d%n",
                    componentId,
                    monitoring.key(),
                    monitoring.value()
                );
        } catch (IOException exception) {
            if (logging)
                System.err.printf(
                    "@%s: an error has occurred: i/o error for host %s:%d%n",
                    componentId,
                    monitoring.key(),
                    monitoring.value()
                );
            exception.printStackTrace();
        }
    }

    private void sendNotification() {
        // opt out of failure retransmission without appropriate flag
        if (!notifySenderOnError)
            return;

        Message failureMessage = new Message();
        String address = getFirstNonLoopbackAddress(true, false);

        // reformat message into failure message
        failureMessage.sender = String.format("mailer@%s", address);
        failureMessage.recipients = message.sender;
        failureMessage.subject = String.format("failed delivery: %s", message.subject);
        failureMessage.data = String.format(
                "for at least one recipient the following message has not been delivered: %s",
                message.data
        );

        // various transformations to find new host for failure message transmission
        String newDomain = message.sender.split("@")[1];

        // attempt failure message transmission
        DynamicDMTPClient dmtpClient = new DynamicDMTPClient(
                String.format("%s-failure-sender", componentId),
                newDomain,
                failureMessage,
                monitoring,
                root,
                false,
                logging
        );

        // run the new failure message sender
        dmtpClient.run();
    }

    /**
     * auxiliary method to go through interfaces and find first (non-loopback) address
     * snippet sourced from:
     * https://stackoverflow.com/questions/901755/how-to-get-the-ip-of-the-computer-on-linux-through-java/901943#901943
     * (slightly modified from source)
     * @param preferIpv4 whether ipv4 is preferred
     * @param preferIPv6 whether ipv6 is preferred
     * @return first non-loopback ipv4, ipv6 address (or first address in general if both are preferred)
     */
    private static String getFirstNonLoopbackAddress(boolean preferIpv4, boolean preferIPv6) {
        Enumeration<NetworkInterface> en;
        try {
            en = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException exception) {
            return "127.0.0.1";
        }

        while (en.hasMoreElements()) {
            NetworkInterface i = (NetworkInterface) en.nextElement();
            for (Enumeration<InetAddress> en2 = i.getInetAddresses(); en2.hasMoreElements();) {
                InetAddress address = en2.nextElement();
                if (!address.isLoopbackAddress()) {
                    if (address instanceof Inet4Address) {
                        if (preferIPv6) {
                            continue;
                        }
                        return address.getHostAddress();
                    }
                    if (address instanceof Inet6Address) {
                        if (preferIpv4) {
                            continue;
                        }
                        return address.getHostAddress();
                    }
                }
            }
        }

        return "127.0.0.1";
    }
}